package ca.uqac.dim.turtledb;

import ca.uqac.dim.turtledb.physical.Fragment;
import ca.uqac.dim.turtledb.physical.Link;
import ca.uqac.dim.turtledb.physical.Network;
import ca.uqac.dim.turtledb.physical.Site;
import java.util.*;
import sun.reflect.generics.reflectiveObjects.NotImplementedException;

/**
 * User: damien Date: 12-12-02 Time: 03:46 To change this template use File |
 * Settings | File Templates.
 */
public class QueryOptimizer extends Network.NetworkMember {

    private Relation query;
    private QueryPlan queryPlan;

    public int getCost(QueryPlan queryPlan) throws Exception {
        return new CostCalculator(queryPlan).getCost();
    }
    private boolean isGoodQuery = false;

    public QueryPlan optimizeQuery(Relation query) throws Exception {
        this.query = query;
        decompose();
        localise();
        //optimize();
        return queryPlan;

    }

    private void decompose() throws Exception {
        //Normalisation
        //TODO : Faire la normalisation

        //Analyse
        isGoodQuery = new QueryAnalyzer().isGoodQuery();
        if (!isGoodQuery) {
            throw new Exception("La requête est erronée.");
        }

        //Simplification
        //new QuerySimplifier().simplifyQuery();

        //Restructuration
        //TODO : Restructurer
    }

    private void localise() throws Exception {
        queryPlan = new QueryLocalisator().localizeQuery();
    }

    private void optimize() {
        new QueryPlanGlobalOptimiser().optimizeQuery();
    }

    private class QueryAnalyzer {

        //Analyse la requete pour verifier qu'il n'y a pas d'incoherence
        //On retourne true si la requete est correcte, false sinon
        public boolean isGoodQuery() {
            //Recuper les tables de FROM
            //Verifier si les attributs du SELECT sont bien dans une des tables recuperees (Si les attributs sont presents dans plusieurs tables, verifier les correspondances)
            //Analyser le contenu de la clause WHERE pour confirmer qu'il n'y a pas d'erreur

            return true;
        }
    }

    private class QuerySimplifier {

        public QuerySimplifier() {
        }

        //Retourne la requete optimisee sous forme de QueryPlan
        public void simplifyQuery() {
            throw new NotImplementedException(); //TODO : implementation
            //Facile me direz-vous !
            //Trouver un moyen de simplifier la requete?
            //C'est pas franchement evident!
            //On abandonne?
        }
    }

    private class QueryLocalisator {

        private int i = 0; //Sert a nommer les tables intermediaires !
        QueryPlan workingOnQueryPlan = new QueryPlan();
        List<VariableTable> leaves = new ArrayList<VariableTable>();
        Map<VariableTable, Set<Fragment>> localisedLeaves = new HashMap<VariableTable, Set<Fragment>>();

        public QueryPlan localizeQuery() throws Exception {
            //Parcourir l'arbre et trouvez les feuilles à localiser

            getLeaves();

            for (VariableTable v : leaves) {
                localizeLeaf(v);
            }

            makeQueryPlan();

            return workingOnQueryPlan;
        }

        private void getLeaves() throws Exception {
            getLeaves(query);
        }

        private void getLeaves(Relation relation) throws Exception {
            if (relation.isLeaf()) {
                leaves.add((VariableTable) relation);
            } else {
                if (relation instanceof NAryRelation) {
                    List<Relation> lesEnfants = ((NAryRelation) relation).getRelation();
                    for (Relation enfant : lesEnfants) {
                        getLeaves(enfant);
                    }
                } else if (relation instanceof BinaryRelation) {
                    getLeaves(((BinaryRelation) relation).getLeft());
                    getLeaves(((BinaryRelation) relation).getRight());
                } else if (relation instanceof UnaryRelation) {
                    getLeaves(((UnaryRelation) relation).m_relation);
                } else {
                    //Autre Type De relation
                    throw new Exception("N'importe quoi ce truc, je sais pas ce que c'est comme relation ca !");
                }
            }
        }

        private void localizeLeaf(VariableTable v) {
            Set<Fragment> fragments = new HashSet<Fragment>();
            for (Site s : getNetwork().getSites()) {
                Fragment fragment = s.getFragments().get(v.getName());
                if (fragment != null) {
                    fragments.add(fragment);
                }
            }
            localisedLeaves.put(v, fragments);
        }

        private void makeQueryPlan() throws Exception {
            move(query);
            elireLeSiteQuiFaitToutLeBoulot(query);
        }

        private void move(Relation query) throws Exception {
            if (query.isLeaf()) {
                makeSubQueryPlan((VariableTable) query);
            } else {
                if (query instanceof NAryRelation) {
                    List<Relation> lesEnfants = ((NAryRelation) query).getRelation();
                    for (Relation enfant : lesEnfants) {
                        move(enfant);
                    }
                } else if (query instanceof BinaryRelation) {
                    move(((BinaryRelation) query).getLeft());
                    move(((BinaryRelation) query).getRight());
                } else if (query instanceof UnaryRelation) {
                    move(((UnaryRelation) query).m_relation);
                } else {
                    //Autre Type De relation
                    throw new Exception("N'importe quoi ce truc, je sais pas ce que c'est comme relation ca !");
                }
            }
        }

        private void makeSubQueryPlan(VariableTable query) throws Exception {
            Set<Fragment> fragments = localisedLeaves.get(query);
            Relation r = getTogetherFragment(fragments);
            Site executor = getExecutorSite(fragments);
            query.setRelation(r);
            query.setSite(executor.getName());
            query.isFragment();
            renameVariableTable(query);
            donnerLeTravail(query, executor);
        }

        private Relation getTogetherFragment(Set<Fragment> fragments) throws Exception {
            Union union = new Union();
            Join join = null;
            Relation aJoindre = null;
            for (Fragment f : fragments) {
                if (f.isHorizontallyFragmented()) {
                    union.addOperand(f.toVariableTable());
                } else if (f.isVerticallyFragmented()) {
                    if (aJoindre == null) {
                        aJoindre = f.toVariableTable();
                    } else {
                        join = new Join();
                        join.setLeft(aJoindre);
                        join.setRight(f.toVariableTable());
                        aJoindre = join;
                    }
                } else {
                    if (fragments.size() != 1) {
                        throw new Exception("Ouhlala ! Quelle fragmentation incohérente !");
                    } else {
                        return f.toVariableTable();
                    }
                }
            }
            if (join != null) {
                union.addOperand(join);
            }
            return union;

        }

        private void renameVariableTable(VariableTable query) {
            query.setName("intermediaryUselessName" + query.m_name + i++ + (new Date()).toGMTString());

        }

        private Site getExecutorSite(Set<Fragment> fragments) throws Exception {
            //TODO : Parcourir les fragments
            //Compter les apparitions d'un site
            //Prendre le site qui apparait le plus souvent
            if (fragments == null || fragments.isEmpty()) {
                throw new Exception("C'est n'importe quoi ! je trouve pas ces fragments !");
            }
            return fragments.iterator().next().getHost();
        }

        private void donnerLeTravail(Relation query, Site executor) {
            if (workingOnQueryPlan.get(executor.getName()) == null) {
                workingOnQueryPlan.put(executor.getName(), new HashSet<Relation>());
            }
            workingOnQueryPlan.get(executor.getName()).add(query);

        }

        private void elireLeSiteQuiFaitToutLeBoulot(Relation query) {
            //TODO : Trouver un truc chouette pour determiner qui fait tout le boulot
            Site siteQuiFaitToutLeBoulot = getNetwork().getSites().get(0);

            //Donner le boulot à ce site        
            donnerLeTravail(query, siteQuiFaitToutLeBoulot);
        }
    }

    private class QueryPlanGlobalOptimiser {

        public void optimizeQuery() {
            horizontallyReduce();
            verticallyReduce();
        }

        private void horizontallyReduce() {
            reduceWithSelection();
            reduceWithJoin();
        }

        private void reduceWithSelection() {
            throw new UnsupportedOperationException("Not yet implemented"); //TODO 
        }

        private void verticallyReduce() {
            throw new UnsupportedOperationException("Not yet implemented"); //TODO
        }

        private void reduceWithJoin() {
            throw new UnsupportedOperationException("Not yet implemented"); //TODO
        }
    }

    private class CostCalculator {

        private QueryPlan queryPlan;

        public CostCalculator(QueryPlan queryPlan) {
            this.queryPlan = queryPlan;
        }

        public int getCost() throws Exception {
            int returnValue = 0;
            //On instancie un parcoureur de relation
            QueryRelationTraverser queryRelationTraverser = new QueryRelationTraverser();
            queryRelationTraverser.traverse();                                            //On parcourt pour trouver les valeurs

            //On additionne
            returnValue += queryRelationTraverser.getCoutTotalCom();
            returnValue += queryRelationTraverser.getCoutTotalStock();

            //On renvoie
            return returnValue;
        }

        private class QueryRelationTraverser {

            private int coutTotalCom = 0;
            private int coutTotalStock = 0;

            public int getCoutTotalCom() {
                return coutTotalCom;
            }

            public int getCoutTotalStock() {
                return coutTotalStock;
            }

            public void traverse() throws Exception {
                for (Map.Entry<String, Set<Relation>> entry : queryPlan.entrySet()) {
                    Site siteExecution = getNetwork().getSiteForName(entry.getKey());
                    for (Relation r : entry.getValue()) {
                        recursionner(siteExecution, r);

                    }
                }
            }

            private void recursionner(Site siteExecution, Relation r) throws Exception {
                if (r.isLeaf()) {
                    sortirLeCoutDUnFragment(siteExecution, r);
                } else {
                    if (r instanceof NAryRelation) {
                        List<Relation> lesEnfants = ((NAryRelation) r).getRelation();
                        for (Relation enfant : lesEnfants) {
                            recursionner(siteExecution, enfant);
                        }
                    } else if (r instanceof BinaryRelation) {
                        recursionner(siteExecution, ((BinaryRelation) r).getLeft());
                        recursionner(siteExecution, ((BinaryRelation) r).getRight());
                    } else if (r instanceof UnaryRelation) {
                        recursionner(siteExecution, ((UnaryRelation) r).m_relation);
                    } else {
                        //Autre Type De relation
                        throw new Exception("N'importe quoi ce truc, je sais pas ce que c'est comme relation ca !");
                    }
                }

            }

            private void sortirLeCoutDUnFragment(Site siteExecution, Relation r) throws Exception {
                if (!r.isFragment()) {
                    r = getFragmentForLeaf((VariableTable) r);
                }
                if (r == null) {
                    throw new Exception("QueryPlan malformed, hein ! heu r'tourn at'baraque");
                } else {
                    Site siteDOrigine = ((Fragment) r).getHost();
                    if (!siteDOrigine.equals(siteExecution)) {
                        List<Link> path = siteDOrigine.getPath(siteExecution);
                        if (path.isEmpty()) {
                            throw new Exception("N'importe quoi ce truc, comment tu le bouges ton fragment !");
                        }
                        Site siteDAvant = siteDOrigine;
                        for (Link lien : path) {
                            //Calculer le cout de communication
                            coutTotalCom += lien.getCoutDeCommunicationUnitaire() * r.getCardinality();

                            //Chercher le site de destination, pour connaitre son cout de stockage
                            Site siteDestination;
                            if (siteDAvant.equals(lien.getSiteDroit())) {
                                siteDestination = lien.getSiteGauche();
                            } else {
                                siteDestination = lien.getSiteDroit();
                            }

                            //Calculer le coût de stockage
                            coutTotalStock += siteDestination.getCoutStockageUnitaire() * r.getCardinality();
                            //Avancer
                            siteDAvant = siteDestination;
                        }
                    }
                }
            }

            private Fragment getFragmentForLeaf(VariableTable v) {
                if (v.getSite() != null && !v.getSite().isEmpty()) {
                    return getFragmentForLeaf(v, getNetwork().getSiteForName(v.getSite()));
                }
                for (Site s : getNetwork().getSites()) {
                    return getFragmentForLeaf(v, s);
                }
                return null;
            }

            private Fragment getFragmentForLeaf(VariableTable v, Site s) {

                Fragment fragment = s.getFragments().get(v.getName());
                if (fragment != null) {
                    return fragment;
                } else {
                    return null;
                }
            }
        }
    }
}
